Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add basic support for LeetCode #114

Open
wants to merge 3 commits into
base: master
Choose a base branch
from

Conversation

usk83
Copy link

@usk83 usk83 commented Oct 16, 2020

related to #21

What this PR have done

  • get problem data
    • include sample test cases extracted from a problem description
  • get service data
  • check log in status
  • submit a program
    • include guess-language-id feature

What this PR does not include

  • login to service
  • contests related features
  • test codes

usk83 added 2 commits October 16, 2020 01:16
- accept `json` as an extra arg
- deal with unknown HTTP response status codes from any of services
### supported

- get-problem
	- no options
- get-service
	- no options
- login-service,
	- check only
- submit-code
- guess-language-id

### not supported

- get-contest
@usk83
Copy link
Author

usk83 commented Oct 16, 2020

@kmyk 追加でいくつかNotesです

  • ログイン機能の実装はスキップしました
    • reCAPTCHA が導入されているのと、leetcode-cliでもログインができない状態になっていて未解決のようであったため
    • ログインが必要な機能についてはcookieにセッションIDやらCSRFトークンやらがセットされた状態で動かしました(開発の際は手動でcookie.jarに記載しました)
  • 問題の sample test cases について、入出力がstdinstdoutでなく、そのままでは使いようがないため、問題文中の当該箇所のテキストを取得したものをそのまま返却しています(特に整形していません)

CIについて

  • format がコケてる理由が分かっていません
    • 手元で pylint などは走らせて問題を解決済みです
  • f-String (f'{variable}'の記法)が Python 3.6 からのサポートでテストに引っかかっているようなのですが、古い記法に書き換えたほうが良いでしょうか?

お手すきの際にご確認いただければ幸いです!

This was referenced Oct 16, 2020
@kmyk
Copy link
Member

kmyk commented Oct 16, 2020

Notes

ログイン機能の実装はスキップしました

了解です。reCAPTCHA の突破を頑張るべきではないので、ユーザには oj コマンド経由の WebDriver を使ってもらうことにしましょう。
これにはサイトごとのログインページの URL を指定することが必要なので、LeetCodeService class に次のメソッドも定義しておいてください。

def get_url_of_login_page(self) -> str:

問題の sample test cases について、入出力がstdin、stdoutでなく、そのままでは使いようがないため、問題文中の当該箇所のテキストを取得したものをそのまま返却しています(特に整形していません)

分かりました。ひとまずはこのままマージして、整形をどうするかは後で考えましょう。

CI

CI の問題の原因はどちらもこのプルリクエストではなかったので、こちらで修正しておきました。rebase してもらえば解決すると思います。

format がコケてる理由が分かっていません

ちょうど発生している問題 (online-judge-tools/verification-helper#335) の影響のようです。ひとまず回避策を入れました (#115)

f-String (f'{variable}'の記法)が Python 3.6 からのサポートでテストに引っかかっているようなのですが、古い記法に書き換えたほうが良いでしょうか?

いいえ、そのままで大丈夫です。Python 3.5 はすでに EOL を迎えている (https://www.python.org/dev/peps/pep-0478/) ので、破壊的変更にはなりますが対応を切ってしまって大丈夫でしょう (#116)

@usk83
Copy link
Author

usk83 commented Oct 16, 2020

@kmyk ご対応ありがとうございます!
get_url_of_login_pageメソッド追加します。
追加で1コミットしたら次のCIはmerge先が最新のmasterになって修正後のCIが走りますでしょうか?
もちろんrebaseしたほうがよければrebaseします:ok_hand:

@usk83
Copy link
Author

usk83 commented Oct 16, 2020

メソッド追加のコミットを追加しました
必要であればrebaseするのでコメントください(必要なければコンテクスト残したほうがいいかなくらいの気持ちです)

Copy link
Member

@kmyk kmyk left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

基本はよさそうです。
しかしいくつか修正や確認しておきたい点があります。スクレイピングツール特有の面倒な議論なのでしんどいのですが、対応お願いします 🙇
また、サンプルダウンロード機能だけでよいのでテストの追加もお願いします。他のファイル (例: https://github.com/online-judge-tools/api-client/blob/master/tests/get_problem_yukicoder.py) をコピペしてすこし修正するだけでできると思います。テストの追加はつい省略したくなりますが、テストがないと「LeetCode の仕様変更で動かなくなり、しかしユーザは誰も報告してくれず、数ヶ月間ずっと動かないままでみんな困っていた」みたいな状況が発生するためです (複数回経験済み)。

CI は走ってるようなので rebase はなしで大丈夫です。

Comment on lines +30 to +44
service_url = self.get_url()
session.headers.update({
'Origin': service_url,
'Referer': service_url,
'Content-type': 'application/json',
})

# get csrf token from cookies and set it to header as well
for cookie in session.cookies:
if cookie.domain == 'leetcode.com' and cookie.name == 'csrftoken':
if cookie.value is not None:
session.headers.update({
'X-CSRFToken': cookie.value,
})
break
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

optional: session は連続して利用するのが期待されるものなので session.headers を書き換えるのはあまりきれいではありません。たとえば以下のように実行すると AtCoder 社のサーバに LeetCode 向けの CSRF token などが送られてしまってセキュリティ的に不適切です。

session = requests.Session()
LeetCodeProblem(...).download_sample_cases(session=session)
AtCoderProblem(...).download_sample_cases(session=session)

utils.request(..., headers={'X-CSRFToken': ..., ...}, session=session) のようにすればその headers が追加されて送られます。

実際のところこの挙動が問題になることはないだろうので、ひとまずコメントに説明を書いておくだけでもかまいません。

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

たとえば以下のように実行すると AtCoder 社のサーバに LeetCode 向けの CSRF token などが送られてしまってセキュリティ的に不適切です。

これ気が回せていませんでした。そのとおりだと思います。
リクエスト時にheadersを渡す方向で考え直してみます。

content_html = json_resp['data']['question']['content']
if content_html is None:
logger.warning("This problem seems to be locked: need premium?")
return []
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

実は SampleParserError というのが用意されています。「0 個のサンプルがあった」ではなく「サンプルの取得に失敗した」だと思うので、こちらを使ってほしいです。表示されるエラーメッセージがすこし分かりやすいものになるはずです。

class SampleParseError(RuntimeError):

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

承知しました、確認します!

if idx < len(children):
output_data = children[idx].strip()

if input_data and output_data:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

input_data and output_data が偽な場合になにか警告を表示してほしいです。ほとんど常に成功するのなら、偽な場合をエラーにしてしまってもよいかもしれません。
「実は5個あるサンプルのうちの1個だけ取得に失敗していて、そのことに気付かなかった」などの事態があるとよくないためです。

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

なるほどです。
こちら如何せん人が読む問題文中から抽出しているため、LeetCode側の不備でブレやエラーがありそうだなと思い、このような実装にしていました。実際に第1問が

Input: nums = [2,7,11,15], target = 9
Output: [0,1]
Output: Because nums[0] + nums[1] == 9, we return [0, 1].

というふうにOutputが2つ書かれていて、2つめはOutputではなく説明になっていたりします。
(第2問では

Input: l1 = [2,4,3], l2 = [5,6,4]
Output: [7,0,8]
Explanation: 342 + 465 = 807.

のようになっています)

ほとんどの場合、テストケースは

Input: ...
Output: ...
Explanation: ...

もしくは

Input: ...
Output: ...

と書かれているかと思うので、これを想定して、そうでない場合にErrorとするのもありかなと思うのですが、その場合、いきなり第1問のサンプルケースが取得できないこともあり、判断に迷っています。

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

第1問からエラーになるのはつらいというのはそうですね。どういうフォーマットがどれくらいの割合なのかは私は分からない (LeetCode にはほとんど参加していない) ので、警告の表示だけかエラーで落とすかの判断は @usk83 さんに任せます。

try:
resp = utils.request('POST', f'https://leetcode.com/problems/{self.title_slug}/submit/', session=session, json=json_body)
except requests.exceptions.HTTPError as e:
if e.response.status_code != 429:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Too Many Requests はどの程度の頻度で起こりますか? web 上から手動で提出した場合でも提出頻度が原因のエラーは発生することはありますか? これが起こるのは api-client 側が原因なのか使うユーザが原因なのかどちらが多いですか?

oj-api から提出だと普通に使っていてもたまに Too Many Requests で弾かれて不便である」「web からの提出ではそのようなことはない」という状況なら retry は実装するべきでしょう。そうでないなら retry はあまりしたくありません。特に「もしユーザが大量に連続提出を試みた場合に Too Many Requests で弾かれる」ということなら retry をすべきではありません。ツールからの自動提出というのは一般にサービス運営側からはあまり歓迎されないものですし、うっかり負荷をかけすぎてジャッジが詰まりコンテストが unrated になったりすると面倒なことになります。

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

特に「もしユーザが大量に連続提出を試みた場合に Too Many Requests で弾かれる」ということなら retry をすべきではありません。ツールからの自動提出というのは一般にサービス運営側からはあまり歓迎されないものですし、うっかり負荷をかけすぎてジャッジが詰まりコンテストが unrated になったりすると面倒なことになります。

なるほどです。


Too Many Requestsのエラーですが、実際にLeetCode上でやってみるとまずSubmitして結果が返ってきてから(ここまで1-3秒)、すぐに(1-3秒程度)再度Submitするとこのエラーになり、自分の場合はたまに遭遇するくらいな感じです。
アカウントがFreeかPremiumかにもよると認識していて、Freeアカウントの場合は上記のような数秒の待機時間があるので、うっかりミスにすぐ気づいて再投稿したりすると本ツールの使用の有無にかかわらずユーザの問題で発生します。
自分が普通にサイト上でやっているときは、このエラーになると通るまで数回連打するようなことがあるのでリトライしてくれたら楽だなと考えましたが、

もしユーザが大量に連続提出を試みた場合に

を考えると無理にretryはしないほうがいいかなという考えが少し強くなりました。
retryはしない方向でいかがでしょうか?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

はい。そういうことなら retry はしない方向がよさそうです。

if json_resp['state'] == 'SUCCESS':
break
logger.warning('Waiting for the result of your submission(id: %s)', submission_id)
time.sleep(1 / 3)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: もうすこし長めに待ちたいです。とりあえず 1 秒待てば許されるはずなのでとりあえず 1 秒にしておきたい。長めに待機を挟むとたいていのユーザからは不満が出るのですが、online-judge-tools のような微妙にグレーなツールでは「サーバに無闇に負荷をかけることはありません」と言えることの方が重要です。

Suggested change
time.sleep(1 / 3)
time.sleep(1)

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

こちら実際のWebサイトのリクエストをみて、肌感同じくらいな感じに設定しました(実際にsubmitしたあと結果をpollingする実装になっている模様)
伸ばしたほうがいいですか?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

こちら実際のWebサイトのリクエストをみて、肌感同じくらいな感じに設定しました(実際にsubmitしたあと結果をpollingする実装になっている模様)

そういうことなら 1/3 秒でも大丈夫だと思います。ただし、このことをコメントとして書いておいてほしいです。

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

承知しました👍

request_test.py Outdated
@@ -0,0 +1,70 @@
import json
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

このファイルは間違えて checkin されていそう。消しましょう

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

完全に油断しました......
これ最新のcommitを編集してforce pushしちゃってもいいでしょうか?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

はい。force push でも削除する commit の追加でもどちらでも大丈夫です

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

このために作成したアカウントなので大きな問題はないのですが、セッションIDとか思い切り載せちゃってましたね。まずいまずい😲

@usk83
Copy link
Author

usk83 commented Oct 16, 2020

@kmyk

しかしいくつか修正や確認しておきたい点があります。スクレイピングツール特有の面倒な議論なのでしんどいのですが、対応お願いします 🙇

ご丁寧にレビューいただきありがたい限りです:bow:
頂いたコメントにコメント返しました。対応箇所の修正が完了したら連絡いたします!

テストがないと「LeetCode の仕様変更で動かなくなり、しかしユーザは誰も報告してくれず、数ヶ月間ずっと動かないままでみんな困っていた」みたいな状況が発生するためです (複数回経験済み)。

ですよね。Pythonでテストを書いたことがなかったのでついサボりました。
サービス側の変更で動かなくなることは十二分に想定されると思うので、テスト追加してみます。

@usk83 usk83 force-pushed the feature/add-leetcode-support branch from d6a38c2 to b4fde5f Compare October 16, 2020 13:41
@koba-e964 koba-e964 linked an issue Aug 14, 2022 that may be closed by this pull request
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Support LeetCode
2 participants